package com.googlecode.maven.plugin.appassembler;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.commons.io.FileUtils;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.artifact.metadata.ArtifactMetadataSource;
import org.apache.maven.artifact.repository.ArtifactRepository;
import org.apache.maven.artifact.repository.ArtifactRepositoryPolicy;
import org.apache.maven.artifact.repository.layout.ArtifactRepositoryLayout;
import org.apache.maven.artifact.repository.layout.DefaultRepositoryLayout;
import org.apache.maven.artifact.repository.layout.FlatRepositoryLayout;
import org.apache.maven.artifact.resolver.ArtifactCollector;
import org.apache.maven.artifact.resolver.ArtifactResolutionResult;
import org.apache.maven.artifact.resolver.ResolutionListener;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugins.annotations.Component;
import org.apache.maven.plugins.annotations.LifecyclePhase;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.plugins.annotations.ResolutionScope;
import org.apache.maven.project.MavenProject;
import org.apache.maven.project.MavenProjectHelper;
import org.apache.maven.repository.RepositorySystem;

/**
 * App assembler mojo.
 */
@Mojo(name = "assemble", requiresDependencyResolution = ResolutionScope.RUNTIME,
        defaultPhase = LifecyclePhase.PACKAGE)
public class AppassemblerMojo
        extends AbstractMojo
    {
    @Parameter(readonly = true, defaultValue = "${project}")
    private MavenProject project;
    @Parameter(readonly = true, defaultValue = "${project.basedir}")
    private File basedir;
    @Parameter(readonly = true, defaultValue = "${project.build.directory}")
    private File targetDirectory;
    @Parameter(readonly = true, defaultValue = "${project.managedVersionMap}")
    private Map managedVersionMap;
    @Parameter(readonly = true, defaultValue = "${project.artifact}")
    private Artifact projectArtifact;
    @Parameter(readonly = true, defaultValue = "${project.artifacts}")
    private Set<Artifact> artifacts;
    @Parameter(readonly = true, defaultValue = "${project.dependencyArtifacts}")
    private Set<Artifact> dependencyArtifacts;
    @Parameter(readonly = true,
            defaultValue = "${project.artifactId}-${project.version}")
    private String outputFilename;
    @Parameter(readonly = true, defaultValue = "${localRepository}")
    private ArtifactRepository localRepository;
    @Component
    private RepositorySystem repositorySystem;
    @Component
    private ArtifactCollector artifactCollector;
    @Component
    private ArtifactMetadataSource artifactMetadataSource;
    @Component
    private MavenProjectHelper projectHelper;
    /**
     * Application name.
     */
    @Parameter(required = true, defaultValue = "${project.artifactId}")
    private String appName;
    /**
     * Application long name.
     */
    @Parameter(defaultValue = "${project.name}")
    private String appLongName;
    /**
     * Application description.
     */
    @Parameter(defaultValue = "${project.description}")
    private String appDescription;
    /**
     * Application assembly target directory.
     */
    @Parameter(required = true,
            defaultValue = "${project.build.directory}/appassembler")
    private File assembleDirectory;
    /**
     * Boot directory name.
     */
    @Parameter(required = true, defaultValue = "boot")
    private String bootDirectory;
    /**
     * Lib directory name.
     */
    @Parameter(required = true, defaultValue = "lib")
    private String libDirectory;
    /**
     * Boot repository layout.
     */
    @Parameter(defaultValue = "default")
    private String bootRepositoryLayout;
    /**
     * Lib repository layout.
     */
    @Parameter(defaultValue = "default")
    private String libRepositoryLayout;
    /**
     * Booter configuration.
     */
    @Parameter
    private BooterConfiguration booter;
    /**
     * Launcher configuration.
     */
    @Parameter
    private LauncherConfiguration launchers;
    /**
     * Main class name.
     */
    @Parameter(required = true)
    private String mainClass;
    /**
     * Resource directories.
     */
    @Parameter
    private List<String> resourceDirectories;
    /**
     * Additional classpaths.
     */
    @Parameter
    private List<String> additionalClasspath;
    private final Map<String, ArtifactRepositoryLayout> availableRepositoryLayouts = new HashMap<String, ArtifactRepositoryLayout>();

    /**
     * Constructs a new AppassemblerMojo object.
     */
    public AppassemblerMojo()
        {
        super();

        availableRepositoryLayouts.put("default", new DefaultRepositoryLayout());
        availableRepositoryLayouts.put("flat", new FlatRepositoryLayout());
        }

    /**
     * Creates an artifact repository.
     *
     * @param layoutName Layout name.
     * @param name Repository name.
     * @param baseDir Repository base directory.
     * @return The artifact repository created.
     * @throws MojoExecutionException If an error occurred.
     * @throws MojoFailureException If an error occurred.
     */
    private ArtifactRepository createRepository(String layoutName, String name,
            File baseDir)
            throws MojoExecutionException, MojoFailureException
        {
        ArtifactRepositoryLayout artifactRepositoryLayout =
                (ArtifactRepositoryLayout) availableRepositoryLayouts.get(
                layoutName);
        if (artifactRepositoryLayout == null)
            {
            throw new MojoFailureException(String.format(
                    "Unknown repository layout '%s'.", layoutName));
            }
        return repositorySystem.createArtifactRepository(name,
                "file:" + baseDir.getPath(),
                artifactRepositoryLayout, new ArtifactRepositoryPolicy(false,
                ArtifactRepositoryPolicy.UPDATE_POLICY_NEVER,
                ArtifactRepositoryPolicy.CHECKSUM_POLICY_WARN),
                new ArtifactRepositoryPolicy(true,
                ArtifactRepositoryPolicy.UPDATE_POLICY_NEVER,
                ArtifactRepositoryPolicy.CHECKSUM_POLICY_WARN));
        }

    /**
     * Collects the dependencies of the specified artifacts.
     *
     * @param artifacts Artifacts whose dependencies are to be collection.
     * @return The dependencies of the specified artifacts.
     */
    private Set<Artifact> collectDependencies(Set<Artifact> artifacts)
        {
        ArtifactResolutionResult result = artifactCollector.collect(
                artifacts, projectArtifact, managedVersionMap, localRepository,
                null, artifactMetadataSource, null,
                new ArrayList<ResolutionListener>(), null);
        return result.getArtifacts();
        }

    /**
     * Copies/deploys an artifact to an artifact repository.
     *
     * @param artifact The artifact to be deployed/copied.
     * @param repository The target artifact repository.
     * @throws IOException If an I/O exception occurred.
     */
    private void deploy(Artifact artifact, ArtifactRepository repository)
            throws IOException
        {
        File sourceFile = new File(localRepository.getBasedir(),
                localRepository.pathOf(artifact));
        File targetFile = new File(repository.getBasedir(), repository.pathOf(
                artifact));
        targetFile.getParentFile().mkdirs();
        FileUtils.copyFile(sourceFile, targetFile);
        }

    /**
     * {@inheritDoc}
     */
    public void execute()
            throws MojoExecutionException, MojoFailureException
        {
        Set<Artifact> bootArtifacts = new HashSet<Artifact>();
        Set<Artifact> libArtifacts = new HashSet<Artifact>();
        List<String> bootArtifactPaths = new ArrayList<String>();
        List<String> libArtifactPaths = new ArrayList<String>();
        Set<Artifact> processedArtifacts = new HashSet<Artifact>();

        ArtifactRepository bootRepository = createRepository(
                bootRepositoryLayout, "boot", new File(assembleDirectory,
                bootDirectory));
        Set<Artifact> providedArtifacts = new HashSet<Artifact>();
        for (Iterator<Artifact> it = dependencyArtifacts.iterator(); it.
                hasNext();)
            {
            Artifact artifact = (Artifact) it.next();
            if (artifact.getScope().equals("provided"))
                {
                if (artifact.getArtifactHandler().isAddedToClasspath())
                    {
                    providedArtifacts.add(artifact);
                    }
                }
            }
        providedArtifacts.addAll(collectDependencies(providedArtifacts));
        for (Artifact artifact : providedArtifacts)
            {
            try
                {
                deploy(artifact, bootRepository);
                processedArtifacts.add(artifact);
                bootArtifacts.add(artifact);
                bootArtifactPaths.add(String.format("%s/%s", bootDirectory,
                        bootRepository.pathOf(artifact)));
                }
            catch (IOException e)
                {
                throw new MojoFailureException(
                        "Error copying artifact to boot repository.", e);
                }
            }

        ArtifactRepository libRepository = createRepository(libRepositoryLayout,
                "lib", new File(assembleDirectory, libDirectory));
        if (!processedArtifacts.contains(projectArtifact))
            {
            try
                {
                if (projectArtifact.getArtifactHandler().isAddedToClasspath())
                    {
                    deploy(projectArtifact, libRepository);
                    processedArtifacts.add(projectArtifact);
                    libArtifacts.add(projectArtifact);
                    libArtifactPaths.add(String.format("%s/%s", libDirectory,
                            bootRepository.pathOf(projectArtifact)));
                    }
                }
            catch (IOException e)
                {
                throw new MojoFailureException(
                        "Error copying artifact to lib repository.", e);
                }
            }
        for (Iterator<Artifact> it = artifacts.iterator(); it.hasNext();)
            {
            Artifact artifact = (Artifact) it.next();
            if (!processedArtifacts.contains(artifact))
                {
                try
                    {
                    if (artifact.getArtifactHandler().isAddedToClasspath())
                        {
                        deploy(artifact, libRepository);
                        processedArtifacts.add(artifact);
                        libArtifacts.add(artifact);
                        libArtifactPaths.add(String.
                                format("%s/%s", libDirectory, bootRepository.
                                pathOf(artifact)));
                        }
                    }
                catch (IOException e)
                    {
                    throw new MojoFailureException(
                            "Error copying artifact to lib repository.", e);
                    }
                }
            }

        Collections.sort(bootArtifactPaths);
        Collections.sort(libArtifactPaths);
        if (additionalClasspath != null)
            {
            bootArtifactPaths.addAll(0, additionalClasspath);
            }

        App app = new App();
        app.setAppName(appName);
        app.setAppLongName(appLongName);
        app.setAppDescription(appDescription);
        app.setBootDirectory(bootDirectory);
        app.setLibDirectory(libDirectory);
        app.setMainClass(mainClass);
        app.setBootArtifacts(bootArtifacts);
        app.setLibArtifacts(libArtifacts);
        app.setBootArtifactPaths(bootArtifactPaths);
        app.setLibArtifactPaths(libArtifactPaths);

        BooterManager.apply(booter, app, assembleDirectory);
        LauncherManager.apply(launchers, app, assembleDirectory);

        File sourceDirectory = new File(basedir, "src/main/appassembler");
        try
            {
            copyResources(sourceDirectory, assembleDirectory);
            if (resourceDirectories != null)
                {
                for (String resourceDirectory : resourceDirectories)
                    {
                    sourceDirectory = new File(basedir, resourceDirectory);
                    copyResources(sourceDirectory, assembleDirectory);
                    }
                }
            }
        catch (IOException e)
            {
            throw new MojoFailureException("Error copying files.", e);
            }

        try
            {
            File outputFile = new File(targetDirectory, String.format("%s.zip",
                    outputFilename));
            Archiver archiver = new ZipArchiver();
            archiver.archive(assembleDirectory, outputFile, app.
                    getExecutableFiles());
            projectHelper.attachArtifact(project, "zip", outputFile);
            }
        catch (IOException e)
            {
            throw new MojoFailureException("Error packaging ZIP.", e);
            }

        try
            {
            File outputFile = new File(targetDirectory, String.format("%s.tgz",
                    outputFilename));
            Archiver archiver = new TgzArchiver();
            archiver.archive(assembleDirectory, outputFile, app.
                    getExecutableFiles());
            projectHelper.attachArtifact(project, "tgz", outputFile);
            }
        catch (IOException e)
            {
            throw new MojoFailureException("Error packaging TGZ.", e);
            }
        }

    /**
     * Copies resources (files) from the source directory to the target
     * directory.
     *
     * The standard file exclusion filter (filtering out source control
     * management files) is applied.
     *
     * @param sourceDirectory Source directory (not included).
     * @param targetDirectory Target directory (not included).
     * @throws IOException If an I/O exception occurred.
     */
    private void copyResources(File sourceDirectory, File targetDirectory)
            throws IOException
        {
        if (sourceDirectory.isDirectory())
            {
            for (File file : sourceDirectory.
                    listFiles(StandardExcludesFileFilter.INSTANCE))
                {
                if (file.isDirectory())
                    {
                    FileUtils.
                            copyDirectory(file, new File(targetDirectory,
                            file.getName()), StandardExcludesFileFilter.INSTANCE);
                    }
                else
                    {
                    FileUtils.copyFileToDirectory(file, targetDirectory);
                    }
                }
            }
        }
    }
